package org.hepeng.workx.spring.cloud.netflix.zuul.fallback;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.netflix.client.ClientException;
import com.netflix.hystrix.exception.HystrixBadRequestException;
import com.netflix.hystrix.exception.HystrixRuntimeException;
import com.netflix.hystrix.exception.HystrixTimeoutException;
import lombok.Getter;
import org.apache.commons.lang3.StringUtils;
import org.hepeng.workx.spring.cloud.netflix.zuul.RequestContextHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider;
import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.ClientHttpResponse;

import java.io.IOException;
import java.io.InputStream;
import java.util.Objects;


/**
 * @author he peng
 */
public class CommonRouteFallbackProvider implements FallbackProvider {

    private static final Logger LOG = LoggerFactory.getLogger(CommonRouteFallbackProvider.class);

    private String route = "*";
    private HttpResponseBodyProcessor bodyProcessor;
    private ApplicationContext applicationContext;

    public CommonRouteFallbackProvider(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
        ObjectMapper objectMapper = this.applicationContext.getBean(ObjectMapper.class);
        this.bodyProcessor = new DefaultRestHttpResponseBodyProcessor(objectMapper);
    }

    public CommonRouteFallbackProvider(String route , ApplicationContext applicationContext) {
        this(applicationContext);
        this.route = route;
    }

    public CommonRouteFallbackProvider(String route , HttpResponseBodyProcessor bodyProcessor) {
        this.route = route;
        this.bodyProcessor = bodyProcessor;
    }

    @Override
    public String getRoute() {
        return this.route;
    }


    public ClientHttpResponse fallbackResponse() {
        return fallbackResponse(null);
    }


    public ClientHttpResponse fallbackResponse(Throwable cause) {
        String route = StringUtils.defaultString((String) RequestContextHelper.getServiceId(), "");
        return fallbackResponse(route , cause);
    }

    protected void handleException(ClientHttpResponseBuilder builder) {
        handleOtherException(builder);
        handleRibbonException(builder);
        handleHystrixException(builder);
    }

    private void handleOtherException(ClientHttpResponseBuilder builder) {
        Throwable cause = builder.getThrowable();
        if (Objects.isNull(cause)) {
            LOG.warn("hystrix fallback executing , routing target [{}] service " +
                            ", no exceptions were thrown", builder.getRoute());
            builder
                    .httpStatus(HttpStatus.INTERNAL_SERVER_ERROR)
                    .statusText("routing to " + builder.getRoute() + " service error , no exceptions thrown");
            return;
        }

        LOG.warn("hystrix fallback executing , routing target [{}] service " +
                        ", exception msg {} "
                , builder.getRoute() , cause.getMessage());
        builder
                .httpStatus(HttpStatus.INTERNAL_SERVER_ERROR)
                .statusText("routing to " + builder.getRoute() + " service error cause : {} "
                        + StringUtils.defaultString(cause.getMessage() , ""));
    }

    private void handleHystrixException(ClientHttpResponseBuilder builder) {
        Throwable cause = builder.getThrowable();
        if (Objects.isNull(cause)) {
            return;
        }

        String route = StringUtils.defaultString(builder.getRoute() , "");
        if (cause instanceof HystrixBadRequestException) {
            LOG.warn("hystrix fallback executing , routing target [{}] service " +
                            ", hystrix bad request exception msg {} "
                    , builder.getRoute() , cause.getMessage());

            builder
                    .httpStatus(HttpStatus.BAD_REQUEST)
                    .statusText("routing to " + route + " service , hystrix bad request , check your request");
        }

        if (cause instanceof HystrixTimeoutException) {
            LOG.warn("hystrix fallback executing , routing target [{}] service " +
                            ", hystrix timeout exception msg {} "
                    , builder.getRoute() , cause.getMessage());

            builder
                    .httpStatus(HttpStatus.REQUEST_TIMEOUT)
                    .statusText("routing to " + route + " service , hystrix timeout");
        }

        if (cause instanceof HystrixRuntimeException) {
            HystrixRuntimeException hre = (HystrixRuntimeException) cause;
            builder.failureType(hre.getFailureType());

            LOG.warn("hystrix fallback executing , routing target [{}] service , failure type {} , hystrix runtime exception msg {} "
                    , builder.getRoute() , builder.getFailureType() , cause.getMessage());

            switch (hre.getFailureType()) {
                case COMMAND_EXCEPTION: {
                    builder
                            .httpStatus(HttpStatus.INTERNAL_SERVER_ERROR)
                            .statusText("routing to " + route + " service , hystrix command error");
                }
                case TIMEOUT: {
                    builder
                            .httpStatus(HttpStatus.REQUEST_TIMEOUT)
                            .statusText("routing to " + route + " service , hystrix timeout");
                } break;
                case SHORTCIRCUIT: {
                    builder
                            .httpStatus(HttpStatus.INTERNAL_SERVER_ERROR)
                            .statusText("routing to " + route + " service , hystrix circuit breaker is open");
                } break;
                case REJECTED_THREAD_EXECUTION: {
                    builder
                            .httpStatus(HttpStatus.INTERNAL_SERVER_ERROR)
                            .statusText("routing to " + route + " service , hystrix thread pool is full");
                } break;
                case REJECTED_SEMAPHORE_EXECUTION: {
                    builder
                            .httpStatus(HttpStatus.INTERNAL_SERVER_ERROR)
                            .statusText("routing to " + route + " service , hystrix semaphore is full");
                } break;
                case BAD_REQUEST_EXCEPTION: {
                    builder
                            .httpStatus(HttpStatus.BAD_REQUEST)
                            .statusText("routing to " + route + " service , hystrix bad request");
                } break;
                case REJECTED_SEMAPHORE_FALLBACK: {
                    builder
                            .httpStatus(HttpStatus.INTERNAL_SERVER_ERROR)
                            .statusText("routing to " + route + " service , hystrix rejected semaphore fallback");
                } break;
                default:
                    throw hre;
            }
        }
    }

    private void handleRibbonException(ClientHttpResponseBuilder builder) {
        Throwable cause = builder.getThrowable();
        if (Objects.isNull(cause)) {
            return;
        }

        if (cause instanceof ClientException) {
            ClientException ce = (ClientException) cause;
            ClientException.ErrorType errorType = ce.getErrorType();

            LOG.warn("hystrix fallback executing , routing target [{}] service , error type {} " +
                            ", ribbon client exception msg {} "
                    , builder.getRoute() , errorType , cause.getMessage());
            builder
                    .httpStatus(HttpStatus.INTERNAL_SERVER_ERROR)
                    .statusText("routing to " + builder.getRoute() + " service ; " +
                            "error type : " + StringUtils.lowerCase(errorType.name()) +
                            " ; error msg : " + cause.getMessage());
        }
    }

    @Override
    public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
        ClientHttpResponseBuilder responseBuilder = ClientHttpResponseBuilder.builder()
                .route(route)
                .throwable(cause);
        handleException(responseBuilder);
        InputStream body = this.bodyProcessor.fallbackBody(responseBuilder);
        responseBuilder.body(body);
        return responseBuilder.build();
    }

    @Getter
    protected static class ClientHttpResponseBuilder {
        private HttpStatus httpStatus;
        private Integer rawStatusCode;
        private String statusText;
        private InputStream body;
        private HttpHeaders httpHeaders;
        private String route;
        private Throwable throwable;
        private HystrixRuntimeException.FailureType failureType;

        public static ClientHttpResponseBuilder builder() {
            return new ClientHttpResponseBuilder();
        }

        public ClientHttpResponseBuilder failureType(HystrixRuntimeException.FailureType failureType) {
            this.failureType = failureType;
            return this;
        }

        public ClientHttpResponseBuilder throwable(Throwable throwable) {
            this.throwable = throwable;
            return this;
        }

        public ClientHttpResponseBuilder route(String route) {
            this.route = route;
            return this;
        }

        public ClientHttpResponseBuilder httpStatus(HttpStatus httpStatus) {
            this.httpStatus = httpStatus;
            return this;
        }

        public ClientHttpResponseBuilder rawStatusCode(Integer rawStatusCode) {
            this.rawStatusCode = rawStatusCode;
            return this;
        }

        public ClientHttpResponseBuilder statusText(String statusText) {
            this.statusText = statusText;
            return this;
        }

        public ClientHttpResponseBuilder body(InputStream body) {
            this.body = body;
            return this;
        }

        public ClientHttpResponseBuilder httpHeaders(HttpHeaders httpHeaders) {
            this.httpHeaders = httpHeaders;
            return this;
        }

        public ClientHttpResponse build() {
            return new ClientHttpResponse() {
                @Override
                public HttpStatus getStatusCode() throws IOException {
                    return ClientHttpResponseBuilder.this.httpStatus;
                }

                @Override
                public int getRawStatusCode() throws IOException {
                    if (Objects.isNull(ClientHttpResponseBuilder.this.rawStatusCode)) {
                        return ClientHttpResponseBuilder.this.httpStatus.value();
                    }
                    return ClientHttpResponseBuilder.this.rawStatusCode;
                }

                @Override
                public String getStatusText() throws IOException {
                    if (StringUtils.isBlank(ClientHttpResponseBuilder.this.statusText)) {
                        return ClientHttpResponseBuilder.this.httpStatus.getReasonPhrase();
                    }
                    return ClientHttpResponseBuilder.this.statusText;
                }

                @Override
                public void close() {

                }

                @Override
                public InputStream getBody() throws IOException {
                    return ClientHttpResponseBuilder.this.body;
                }

                @Override
                public HttpHeaders getHeaders() {
                    return ClientHttpResponseBuilder.this.httpHeaders;
                }
            };
        }
    }

    protected interface HttpResponseBodyProcessor {

        InputStream fallbackBody(ClientHttpResponseBuilder builder);
    }

}
